iT邦幫忙

2026 iThome 鐵人賽

DAY 4
0
Software Development

Kotlin Ktor 實戰 101系列 第 4

Kotlin Ktor 實戰 101 Day 04 Application、Module 與 Engine

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20260909/20121948LfQo3aVCtG.jpg

day 02 寫下這行程式的時候,我們把它當成固定寫法帶過

embeddedServer(Netty, port = 8080, module = Application::module).start(wait = true)

專案可以跑、測試也建好了,這篇回頭把這行拆開來看,它裡面其實有 3 個角色,Application 是框架本體、module 是組裝它的步驟、Netty 是負責網路的 engine,最後我們會用一個實驗證明這個分工是真的,把 engine 從 Netty 整顆換成 CIO,module() 一行都不用改

這篇要完成什麼

  • 拆解 embeddedServer 那行裡的 3 個角色,Applicationmodule 與 engine
  • 弄清楚 server.coreserver.netty 為什麼拆成 2 個模組
  • 實測把 engine 從 Netty 換成 CIO,確認 module() 和既有測試都不用動
  • 對照 Relix 手刻時是怎麼切這條邊界的

先講一下測試的部分,這篇沒有新增測試,既有的 3 個測試只驗證 Application 行為,testApplication 不會啟動 CIO,CIO 能不能真的監聽 port,會另外用 ./gradlew run 和 curl 確認,兩邊合起來,才足以說明換掉 engine 後 application 行為不變

Application,框架的本體

Application 定義在 server.core 模組裡,也就是 day 02 加的 ktorLibs.server.core 那個相依,整個系列會做的事幾乎都掛在它身上,day 02 註冊的 routing 是掛在它身上,之後每篇要裝的 plugin 也是裝在它身上

要注意的是它的範圍,Application 不負責監聽 port,也不負責解析網路上的 bytes,它只管「拿到一個請求之後,該怎麼處理、怎麼回應」,網路那一段是別人的事

module,組裝 Application 的步驟

再看 day 02 寫在 Application.kt 裡的另一半

fun Application.module() {
    routing {
        get("/") {
            call.respondText("Hello, Ktor!")
        }
    }
}

moduleApplication 的 extension function,extension function 可以在不改類別本身的情況下幫它加方法,函式裡的 this 就是那個 Application 實體,所以 routing { } 實際上是 this.routing { },路由是掛在這個 application 上的

module 這個名字不是關鍵字,改叫別的也能跑,它是官方慣例的命名,這個系列照著用,它的工作就一件,把這個 application 該有的東西裝上去,現在只有 routing,之後 plugin、設定都會進到這裡

然後是 module = Application::module 這段,:: 是函式參考 (function reference),傳給 embeddedServer 的不是「執行 module() 的結果」,而是 module 這個函式本身,你呼叫 start(wait = true) 之後,Ktor 先把 application 建立好,拿著這個函式對它執行一次,engine 才開始監聽 port,組裝發生在啟動流程裡,不是發生在你寫那行程式的當下,這也解釋了 day 03 測試裡 application { module() } 那段在做什麼,測試是自己建了一個 application,再把同一個組裝步驟套上去,所以測試走到的路由跟正式跑起來的是同一條

Engine,負責網路的那一層

Netty 定義在 server.netty 模組裡,engine 的工作全部在 application 之外,監聽 port、接受連線、把網路上的 HTTP bytes 解析成請求物件、交給 application 處理,再把 application 給的回應寫回網路上

client
  │  HTTP bytes
  ▼
┌────────────────────────┐
│ Engine (server.netty)  │  監聽 port、解析 HTTP、寫回回應
└───────────┬────────────┘
            │  請求物件
            ▼
┌────────────────────────┐
│ Application            │  routing、plugin,決定怎麼回應
│ (server.core)          │
└────────────────────────┘

所以 day 02 加 Ktor 相依時是 2 行,不是套件切得比較碎而已,server.coreserver.netty 這條模組邊界,正好就是「框架處理請求」和「網路傳輸」的邊界,embeddedServer 的第 1 個參數吃的是 engine factory,Netty 就是那個 factory 物件,既然是參數,理論上就可以換,接下來實際換一次

把 engine 換成 CIO

CIO 是 Ktor 另一個官方 engine,細節後面「Engine 怎麼選」那節再說,這裡先拿它當實驗對象,如果前面講的邊界是真的,engine 換掉之後 application 那層應該一動都不用動

第 1 步,在 build.gradle.ktsdependencies 區塊加一行

implementation(ktorLibs.server.cio)

第 2 步,Application.kt 只改 2 處,import 那行從 io.ktor.server.netty.Netty 換成 io.ktor.server.cio.CIOmain 裡的第 1 個參數從 Netty 換成 CIO

import io.ktor.server.cio.CIO

fun main() {
    embeddedServer(CIO, port = 8080, module = Application::module).start(wait = true)
}

module() 完全不動,一個字都沒改。跑起來

./gradlew run

我實測的啟動 log 是

> Task :run
16:42:09.705 [main] INFO io.ktor.server.Application -- Autoreload is disabled because the development mode is off.
16:42:09.726 [main] INFO io.ktor.server.Application -- Application started in 0.128 seconds.
16:42:09.740 [DefaultDispatcher-worker-2] INFO io.ktor.server.Application -- Responding at http://0.0.0.0:8080

打個請求

curl http://localhost:8080/
Hello, Ktor!

回應跟 Netty 版一模一樣,log 第 1 行的 Autoreload is disabled 這句也順便說一下,Ktor 有一個 development mode,開了之後程式碼變動可以自動重新載入,目前沒開所以它提醒了一句

測試不驗證 CIO,但一個都不用改

換完 engine 直接跑測試,測試檔完全不動

./gradlew test
> Task :test

ApplicationTest > root path responds hello() PASSED

ApplicationTest > unknown path responds not found() PASSED

EnvironmentTest > Ktor EmbeddedServer class is available() PASSED

BUILD SUCCESSFUL in 1s
4 actionable tasks: 3 executed, 1 up-to-date
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.7.1/userguide/configuration_cache_enabling.html

3 個測試全部通過,這不意外,day 03 講過 testApplication 不啟動真的 engine、不綁 port,它測的是 Application 這一層,engine 換掉對它沒有影響,這正好從另一個方向驗證了 core 和 engine 的邊界,測試站在邊界的 application 這一側,另一側的 engine 怎麼換都碰不到它

實驗做完換回 Netty

這個系列的基線是 Netty,實驗確認完就還原,import 和 embeddedServer 的第 1 個參數改回 Nettybuild.gradle.kts 那行 ktorLibs.server.cio 也拿掉,再跑一次 ./gradlew test 確認 3 個測試照樣通過,專案就回到 day 03 結束時的狀態

Engine 怎麼選

Ktor 官方提供好幾個 engine 實作,常見的有這些

  • Netty,最常見的預設選擇,底層是 JVM 上成熟的 Netty 網路框架,文件和社群範例最多
  • CIO,Coroutine-based I/O,純 Kotlin coroutine 實作,不用額外帶 Netty 這個外部網路框架,相依比較乾淨
  • JettyTomcat,部署環境已經綁定這 2 個容器的話可以直接沿用

這個系列用 Netty,理由很實際,遇到問題時查得到的資料最多,不過經過這篇的實驗你也知道了,這個決定不是單行道,哪天要換,module() 裡累積的所有東西都跟著你走,要動的就是 build.gradle.kts 加一行對應的 engine 相依,再把 Application.kt 的 import 和 embeddedServer 第 1 個參數改掉,就這樣而已

常見陷阱

  • 換了 CIO 但忘了加相依

embeddedServer(CIO, ...) 改了,build.gradle.ktsktorLibs.server.cio 沒加,import io.ktor.server.cio.CIO 會直接編譯失敗,unresolved reference

跟 Relix 的對照

Relix 在 day 06 做過同一條邊界的拆分,那篇把原本混在一起的 RelixApplication 拆成兩半,拆完之後它變成「一個不知道 HTTP server 存在的東西」,連 import com.sun.net.httpserver 都沒有,監聽 port、把 HttpExchange 轉成框架的請求物件,把回應寫回去,這些全部搬進 JdkHttpServerAdaptermain 也變成 2 行,一行組裝 application,一行把它插上 server,當時分開最主要的理由是可測性,RelixApplication 從 server 上拆下來之後才測得動,TestKit 才有得寫

Ktor 把同一條邊界直接做成產品層級的抽象,core 和 engine 拆成 2 個獨立模組,engine 是傳進 embeddedServer 的 factory 參數,而且還多做了一件 Relix 沒做的事,官方維護多個 engine 實作讓你選,Relix 的 adapter 只有 JDK HttpServer 1 個,邊界切出來主要是給測試用的,Ktor 的這條邊界則是兩頭都兌現了,day 03 看到的是可測,這篇看到的是可換,同一條邊界,2 個好處


小結

./gradlew run 和 curl 確認 CIO 能真的啟動與回應,3 個 testApplication 測試則確認 Application 的行為沒變,2 種驗證各管一邊,module() 一行不動,換 engine 的成本就只有 build.gradle.kts 加一行相依,加上 Application.kt 的 import 和 embeddedServer 第 1 個參數,最後專案換回 Netty


下一篇

下一篇拆 day 02 帶過的另一個部分,routing { get("/") { ... } } 這段 DSL,路由怎麼註冊、path 和 method 怎麼對應 handler、這種寫法為什麼能成立,todo-api 的第 1 個端點也會在那篇開出來


參考資料


同步刊登於 Blog

圖片來源:AI 產生


上一篇
Kotlin Ktor 實戰 101 Day 03 用 testApplication 寫下第一個測試
下一篇
Kotlin Ktor 實戰 101 Day 05 Routing 基礎,Todo API 的第一個端點
系列文
Kotlin Ktor 實戰 1017
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言